package org.polyforms.repository.executor.support;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

import javax.inject.Inject;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;

import org.polyforms.repository.executor.NameBasedExecutor;
import org.polyforms.repository.spi.QueryParameterResolver;
import org.polyforms.repository.spi.QueryParameterResolver.Parameter;
import org.polyforms.repository.strategy.QueryNamingStrategy;

/**
 * Abstract implementation of methods which use JPA {@link Query} to do some work in persistence.
 * 
 * @author Kuisong Tong
 * @since 1.0
 */
public abstract class QueryExecutor implements NameBasedExecutor {
    private static final Comparator<Parameter> PARAMETER_COMPARATOR = new Comparator<Parameter>() {
        /**
         * {@inheritDoc}
         */
        public int compare(final Parameter p1, final Parameter p2) {
            return p1.name().compareToIgnoreCase(p2.name());
        }
    };

    @PersistenceContext
    private EntityManager entityManager;

    @Inject
    private QueryParameterResolver parameterResolver;

    @Inject
    private Set<QueryNamingStrategy> queryNamingStrategies;

    /**
     * {@inheritDoc}
     */
    public final Object execute(final Object target, final Method method, final Object... arguments) throws Throwable {
        final Query query = createQuery(method);
        setParameters(query, arguments);
        return getResult(query);
    }

    private Query createQuery(final Method method) {
        final List<String> queryNames = getQueryName(method);
        for (final String queryName : queryNames) {
            try {
                return entityManager.createNamedQuery(queryName);
            } catch (final IllegalArgumentException e) {
                // IGNORE to check more query name
            }
        }
        throw new IllegalArgumentException("query doesn't define for " + method);
    }

    private List<String> getQueryName(final Method method) {
        final List<String> queryNames = new ArrayList<String>();
        for (final QueryNamingStrategy queryNamingStrategy : queryNamingStrategies) {
            queryNames.addAll(queryNamingStrategy.getQueryName(method, this));
        }
        return queryNames;
    }

    private void setParameters(final Query query, final Object[] parameters) {
        final Parameter[] namedParameters = parameterResolver.getNamedParameters(query);
        if (namedParameters.length == 0) {
            setPositionalParams(query, parameters);
        } else {
            Arrays.sort(namedParameters, PARAMETER_COMPARATOR);
            final Map<String, Object> namedArguments = new HashMap<String, Object>();
            for (int i = 0, j = 0; i < namedParameters.length; i++) {
                namedArguments.put(namedParameters[i].name(), parameters[j++]);
            }
            setNamedParams(query, namedArguments);
        }
    }

    private void setPositionalParams(final Query namedQuery, final Object[] parameters) {
        for (int i = 0; i < parameters.length; i++) {
            namedQuery.setParameter(i + 1, parameters[i]);
        }
    }

    private void setNamedParams(final Query namedQuery, final Map<String, Object> parameters) {
        for (final Entry<String, Object> parameter : parameters.entrySet()) {
            namedQuery.setParameter(parameter.getKey(), parameter.getValue());
        }
    }

    protected abstract Object getResult(Query query);
}
